/*
 * Copyright (c) 2000, 2003, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package javax.imageio.stream;

import java.util.ArrayList;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;

/**
 * Package-visible class consolidating common code for
 * <code>MemoryCacheImageInputStream</code> and
 * <code>MemoryCacheImageOutputStream</code>.
 * This class keeps an <code>ArrayList</code> of 8K blocks,
 * loaded sequentially.  Blocks may only be disposed of
 * from the index 0 forward.  As blocks are freed, the
 * corresponding entries in the array list are set to
 * <code>null</code>, but no compacting is performed.
 * This allows the index for each block to never change,
 * and the length of the cache is always the same as the
 * total amount of data ever cached.  Cached data is
 * therefore always contiguous from the point of last
 * disposal to the current length.
 *
 * <p> The total number of blocks resident in the cache must not
 * exceed <code>Integer.MAX_VALUE</code>.  In practice, the limit of
 * available memory will be exceeded long before this becomes an
 * issue, since a full cache would contain 8192*2^31 = 16 terabytes of
 * data.
 *
 * A <code>MemoryCache</code> may be reused after a call
 * to <code>reset()</code>.
 */
class MemoryCache {

  private static final int BUFFER_LENGTH = 8192;

  private ArrayList cache = new ArrayList();

  private long cacheStart = 0L;

  /**
   * The largest position ever written to the cache.
   */
  private long length = 0L;

  private byte[] getCacheBlock(long blockNum) throws IOException {
    long blockOffset = blockNum - cacheStart;
    if (blockOffset > Integer.MAX_VALUE) {
      // This can only happen when the cache hits 16 terabytes of
      // contiguous data...
      throw new IOException("Cache addressing limit exceeded!");
    }
    return (byte[]) cache.get((int) blockOffset);
  }

  /**
   * Ensures that at least <code>pos</code> bytes are cached,
   * or the end of the source is reached.  The return value
   * is equal to the smaller of <code>pos</code> and the
   * length of the source.
   */
  public long loadFromStream(InputStream stream, long pos)
      throws IOException {
    // We've already got enough data cached
    if (pos < length) {
      return pos;
    }

    int offset = (int) (length % BUFFER_LENGTH);
    byte[] buf = null;

    long len = pos - length;
    if (offset != 0) {
      buf = getCacheBlock(length / BUFFER_LENGTH);
    }

    while (len > 0) {
      if (buf == null) {
        try {
          buf = new byte[BUFFER_LENGTH];
        } catch (OutOfMemoryError e) {
          throw new IOException("No memory left for cache!");
        }
        offset = 0;
      }

      int left = BUFFER_LENGTH - offset;
      int nbytes = (int) Math.min(len, (long) left);
      nbytes = stream.read(buf, offset, nbytes);
      if (nbytes == -1) {
        return length; // EOF
      }

      if (offset == 0) {
        cache.add(buf);
      }

      len -= nbytes;
      length += nbytes;
      offset += nbytes;

      if (offset >= BUFFER_LENGTH) {
        // we've filled the current buffer, so a new one will be
        // allocated next time around (and offset will be reset to 0)
        buf = null;
      }
    }

    return pos;
  }

  /**
   * Writes out a portion of the cache to an <code>OutputStream</code>.
   * This method preserves no state about the output stream, and does
   * not dispose of any blocks containing bytes written.  To dispose
   * blocks, use {@link #disposeBefore <code>disposeBefore()</code>}.
   *
   * @throws IndexOutOfBoundsException if any portion of the requested data is not in the cache
   * (including if <code>pos</code> is in a block already disposed), or if either <code>pos</code>
   * or <code>len</code> is < 0.
   */
  public void writeToStream(OutputStream stream, long pos, long len)
      throws IOException {
    if (pos + len > length) {
      throw new IndexOutOfBoundsException("Argument out of cache");
    }
    if ((pos < 0) || (len < 0)) {
      throw new IndexOutOfBoundsException("Negative pos or len");
    }
    if (len == 0) {
      return;
    }

    long bufIndex = pos / BUFFER_LENGTH;
    if (bufIndex < cacheStart) {
      throw new IndexOutOfBoundsException("pos already disposed");
    }
    int offset = (int) (pos % BUFFER_LENGTH);

    byte[] buf = getCacheBlock(bufIndex++);
    while (len > 0) {
      if (buf == null) {
        buf = getCacheBlock(bufIndex++);
        offset = 0;
      }
      int nbytes = (int) Math.min(len, (long) (BUFFER_LENGTH - offset));
      stream.write(buf, offset, nbytes);
      buf = null;
      len -= nbytes;
    }
  }

  /**
   * Ensure that there is space to write a byte at the given position.
   */
  private void pad(long pos) throws IOException {
    long currIndex = cacheStart + cache.size() - 1;
    long lastIndex = pos / BUFFER_LENGTH;
    long numNewBuffers = lastIndex - currIndex;
    for (long i = 0; i < numNewBuffers; i++) {
      try {
        cache.add(new byte[BUFFER_LENGTH]);
      } catch (OutOfMemoryError e) {
        throw new IOException("No memory left for cache!");
      }
    }
  }

  /**
   * Overwrites and/or appends the cache from a byte array.
   * The length of the cache will be extended as needed to hold
   * the incoming data.
   *
   * @param b an array of bytes containing data to be written.
   * @param off the starting offset withing the data array.
   * @param len the number of bytes to be written.
   * @param pos the cache position at which to begin writing.
   * @throws NullPointerException if <code>b</code> is <code>null</code>.
   * @throws IndexOutOfBoundsException if <code>off</code>, <code>len</code>, or <code>pos</code>
   * are negative, or if <code>off+len > b.length</code>.
   */
  public void write(byte[] b, int off, int len, long pos)
      throws IOException {
    if (b == null) {
      throw new NullPointerException("b == null!");
    }
    // Fix 4430357 - if off + len < 0, overflow occurred
    if ((off < 0) || (len < 0) || (pos < 0) ||
        (off + len > b.length) || (off + len < 0)) {
      throw new IndexOutOfBoundsException();
    }

    // Ensure there is space for the incoming data
    long lastPos = pos + len - 1;
    if (lastPos >= length) {
      pad(lastPos);
      length = lastPos + 1;
    }

    // Copy the data into the cache, block by block
    int offset = (int) (pos % BUFFER_LENGTH);
    while (len > 0) {
      byte[] buf = getCacheBlock(pos / BUFFER_LENGTH);
      int nbytes = Math.min(len, BUFFER_LENGTH - offset);
      System.arraycopy(b, off, buf, offset, nbytes);

      pos += nbytes;
      off += nbytes;
      len -= nbytes;
      offset = 0; // Always after the first time
    }
  }

  /**
   * Overwrites or appends a single byte to the cache.
   * The length of the cache will be extended as needed to hold
   * the incoming data.
   *
   * @param b an <code>int</code> whose 8 least significant bits will be written.
   * @param pos the cache position at which to begin writing.
   * @throws IndexOutOfBoundsException if <code>pos</code> is negative.
   */
  public void write(int b, long pos) throws IOException {
    if (pos < 0) {
      throw new ArrayIndexOutOfBoundsException("pos < 0");
    }

    // Ensure there is space for the incoming data
    if (pos >= length) {
      pad(pos);
      length = pos + 1;
    }

    // Insert the data.
    byte[] buf = getCacheBlock(pos / BUFFER_LENGTH);
    int offset = (int) (pos % BUFFER_LENGTH);
    buf[offset] = (byte) b;
  }

  /**
   * Returns the total length of data that has been cached,
   * regardless of whether any early blocks have been disposed.
   * This value will only ever increase.
   */
  public long getLength() {
    return length;
  }

  /**
   * Returns the single byte at the given position, as an
   * <code>int</code>.  Returns -1 if this position has
   * not been cached or has been disposed.
   */
  public int read(long pos) throws IOException {
    if (pos >= length) {
      return -1;
    }

    byte[] buf = getCacheBlock(pos / BUFFER_LENGTH);
    if (buf == null) {
      return -1;
    }

    return buf[(int) (pos % BUFFER_LENGTH)] & 0xff;
  }

  /**
   * Copy <code>len</code> bytes from the cache, starting
   * at cache position <code>pos</code>, into the array
   * <code>b</code> at offset <code>off</code>.
   *
   * @throws NullPointerException if b is <code>null</code>
   * @throws IndexOutOfBoundsException if <code>off</code>, <code>len</code> or <code>pos</code> are
   * negative or if <code>off + len > b.length</code> or if any portion of the requested data is not
   * in the cache (including if <code>pos</code> is in a block that has already been disposed).
   */
  public void read(byte[] b, int off, int len, long pos)
      throws IOException {
    if (b == null) {
      throw new NullPointerException("b == null!");
    }
    // Fix 4430357 - if off + len < 0, overflow occurred
    if ((off < 0) || (len < 0) || (pos < 0) ||
        (off + len > b.length) || (off + len < 0)) {
      throw new IndexOutOfBoundsException();
    }
    if (pos + len > length) {
      throw new IndexOutOfBoundsException();
    }

    long index = pos / BUFFER_LENGTH;
    int offset = (int) pos % BUFFER_LENGTH;
    while (len > 0) {
      int nbytes = Math.min(len, BUFFER_LENGTH - offset);
      byte[] buf = getCacheBlock(index++);
      System.arraycopy(buf, offset, b, off, nbytes);

      len -= nbytes;
      off += nbytes;
      offset = 0; // Always after the first time
    }
  }

  /**
   * Free the blocks up to the position <code>pos</code>.
   * The byte at <code>pos</code> remains available.
   *
   * @throws IndexOutOfBoundsException if <code>pos</code> is in a block that has already been
   * disposed.
   */
  public void disposeBefore(long pos) {
    long index = pos / BUFFER_LENGTH;
    if (index < cacheStart) {
      throw new IndexOutOfBoundsException("pos already disposed");
    }
    long numBlocks = Math.min(index - cacheStart, cache.size());
    for (long i = 0; i < numBlocks; i++) {
      cache.remove(0);
    }
    this.cacheStart = index;
  }

  /**
   * Erase the entire cache contents and reset the length to 0.
   * The cache object may subsequently be reused as though it had just
   * been allocated.
   */
  public void reset() {
    cache.clear();
    cacheStart = 0;
    length = 0L;
  }
}
